"""
Author: Morphlng
Date: 2024-01-23 13:37:54
LastEditTime: 2024-12-18 10:28:29
LastEditors: Morphlng
Description: Helper functions and decorators.
FilePath: /DrivingGym/src/driving_gym/misc/util.py
"""

from __future__ import annotations

import functools
import logging
import os
import sys
import time
import weakref
from importlib import import_module


def dynamic_import(component_type, root_dir, args, suffix="_scenario", init=True):
    """Generic setup function for components (scenarios, planners, etc.)

    Args:
        component_type: Type or path of the component to initialize
        root_dir: Directory to look for the component
        args: Arguments to pass to the component constructor
        suffix: Suffix to append to module name if not provided
        init: Whether to initialize the component
    """
    if component_type is None:
        return None

    if root_dir not in sys.path:
        sys.path.append(root_dir)

    if isinstance(component_type, type):
        return component_type if not init else component_type(*args)
    elif isinstance(component_type, str):
        if component_type.endswith(".py"):
            module_name = os.path.basename(component_type).split(".")[0]
            sys.path.append(os.path.dirname(component_type))
            module = import_module(module_name)
            class_name = module.__name__.title().replace("_", "")
        else:
            if not component_type.endswith(suffix):
                component_type += suffix
            module = import_module(component_type)
            class_name = component_type.split(".")[-1].title().replace("_", "")

        if init:
            return getattr(module, class_name)(*args)
        else:
            return getattr(module, class_name, None)
    else:
        raise ValueError(f"Invalid type {component_type}")


def weak_lru(maxsize: int = 128, typed: bool = False):
    """LRU Cache decorator that keeps a weak reference to 'self'"""

    def wrapper(func):
        @functools.lru_cache(maxsize, typed)
        def _func(_self, *args, **kwargs):
            return func(_self(), *args, **kwargs)

        @functools.wraps(func)
        def inner(self, *args, **kwargs):
            return _func(weakref.ref(self), *args, **kwargs)

        return inner

    return wrapper


def retry(
    exceptions: "Exception | tuple[Exception, ...]",
    tries: int = -1,
    delay: float = 0,
    logger: logging.Logger = None,
):
    """Return a retry decorator

    Args:
        exceptions (Exception | tuple[Exception, ...]): exceptions to be caught.
        tries (int, optional): number of tries. Defaults to -1 (infinite).
        delay (float, optional): delay between tries. Defaults to 0.
        logger (logging.Logger, optional): logger to log the exceptions. Defaults to None.
    """

    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            _tries, _delay = tries, delay
            attempt = 0
            while _tries == -1 or attempt < _tries:
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    attempt += 1
                    if logger:
                        logger.warning(
                            "Attempt %d for %s failed with %s, %d tries left.",
                            attempt,
                            func.__name__,
                            e,
                            max(_tries - attempt, 0),
                        )

                    if _delay > 0:
                        time.sleep(_delay)

                    if _tries != -1 and attempt >= _tries:
                        raise  # Reraising the last exception if all tries are exhausted

        return wrapper

    return decorator


def override(cls):
    """Decorator for documenting method overrides.

    Args:
        cls: The superclass that provides the overridden method. If this
            cls does not actually have the method, an error is raised.
    """

    def check_override(method):
        if method.__name__ not in dir(cls):
            raise NameError("{} does not override any method of {}".format(method, cls))
        return method

    return check_override


def norm_angle(angle: float) -> float:
    """Normalize an angle to the range [-180, 180) degrees.

    Args:
        angle (float): The angle to be normalized, in degrees.

    Returns:
        float: The normalized angle, in degrees.
    """
    return (angle + 180) % 360 - 180


class _NotProvided:
    """Singleton class to provide a "not provided" value for config signatures.

    Using the only instance of this class indicates that the user does NOT wish to
    change the value of some property.
    """

    class __NotProvided:
        pass

    instance = None

    def __init__(self):
        if _NotProvided.instance is None:
            _NotProvided.instance = _NotProvided.__NotProvided()


NotProvided = _NotProvided()
